/**
 * com.chaeron.GPS - An API for access to GPS devices.
 *
 *
 * Copyright (c) 2001, 2002 Chaeron Corporation,
 * All rights reserved.
 * 
 * Chaeron grants you a non-exclusive license to use, modify and re-distribute
 * this program under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version, provided that this copyright notice and 
 * license appear on all copies of the software.
 * 
 * Software is provided "AS IS," without a warranty of any kind. ALL EXPRESS OR
 * IMPLIED REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE
 * HEREBY EXCLUDED. THE ENTIRE RISK ARISING OUT OF USING THE SOFTWARE IS ASSUMED
 * BY THE LICENSEE. See the GNU General Public License for more details.
 *
 * CHAERON AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY
 * LICENSEE OR ANY THIRD PARTY AS A RESULT OF USING OR DISTRIBUTING SOFTWARE.
 * IN NO EVENT WILL CHAERON OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE,
 * PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL
 * OR PUNITIVE DAMAGES, HOWEVER CAUSED AN REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF CHAERON HAS
 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

 * @version     1.02
 * @author  	Andrzej Jan Taramina, Chaeron Corporation
 *  			andrzej@chaeron.com
 */

package com.chaeron.GPS.Garmin;

//***************************************************************************
//*
//* 	Imports
//*
//***************************************************************************/

import com.chaeron.GPS.*;
import com.chaeron.GPS.event.*;
import com.chaeron.GPS.exception.*;

import com.chaeron.platform.common.*;

//***************************************************************************
//*
//* 	GarminGPSImpl Class definition
//*
//***************************************************************************/

/**
 * The GarminGPSImpl class implements Garmin-specific protocol functions
 *
 */

public class GarminGPSImpl extends GPS
{
	
	//***************************************************************************
	//*
	//* 	Constant definitions
	//*
	//***************************************************************************/
	
	final static byte			BEGIN_PACKET					= 16; // DLE
	final static byte			END_PACKET1						= 16; // DLE
	final static byte			END_PACKET2						= 3;  // ETX
	
	
	//***************************************************************************
	//*
	//* 	Attributes
	//*
	//***************************************************************************/
	
	// Garmin Protocol Data Handler object
	
	private Protocol					protocol					= new Protocol();
	
	// Standard packets
	
	private AckGarminPacket				ack							= new AckGarminPacket();
	private NackGarminPacket			nack						= new NackGarminPacket();
	private ProductReqGarminPacket		productRequest				= new ProductReqGarminPacket();
	private ProductDataGarminPacket		productData					= null;
	
	private WaypointTransferHandler		waypointTransferHandler		= null;
	private RouteTransferHandler		routeTransferHandler		= null;
	private TrackTransferHandler		trackTransferHandler		= null;
	
	
	//***************************************************************************
	//*
	//* 	Constructors
	//*
	//***************************************************************************/
	
	/**
	 * Create an GarminGPSImpl instance 
	 *
	 */
	
	public GarminGPSImpl()
	{
		try {
			setSerialBaudRate( 9600 );
		}
		catch( GPSOperationUnavailableException e ) {
		}
	}
	
	
	//***************************************************************************
	//*
	//* 	Accessor Methods
	//*
	//***************************************************************************/
	
	/**
	 * Get current Protocol object
	 *
	 * @return		protocol object
	 */
	
	Protocol getProtocol()
	{
		return( protocol );
	}
	
	//***************************************************************************
	//*
	//* 	Comm Abstract Methods (implemented/overriden)
	//*
	//***************************************************************************/
	
	/**
	 * Connecting the GPS.  
	 */
	
	protected void connectingNotify() throws GPSSerialPortException, GPSOperationUnavailableException
	{
		
	}
	
	
	/**
	 * Connected the GPS.  
	 */
	
	protected void connectedNotify()
	{
		initialize();
	}
	
	
	/**
	 * Disconnecting the GPS.  
	 */
	
	protected void disconnectingNotify()
	{
		
	}
	
	
	/**
	 * Disconnected the GPS.  
	 */
	
	protected void disconnectedNotify()
	{
		
	}
	
	
	/**
	 * Check if a byte is a Begin Packet. 
	 *
	 * @param	chr		byte to be checked
	 * @return			is byte begin packet?
	 */
	
	protected boolean isBeginPacketByte( byte chr )
	{
		return( chr == BEGIN_PACKET );
	}
	
	
	/**
	 * Check if a byte is a End Packet. 
	 *
	 * @param	chr		byte to be checked
	 * @return			is byte end packet?
	 */
	
	protected boolean isEndPacketByte( byte chr )
	{
		return( chr == END_PACKET1 );
	}
	
	
	/**
	 * Check if a duplicate End Packet byte should be ignored, that is, treated as an "escape"
	 * sequence to allow embedding of and End Packet byte in the data catalog.  
	 *
	 * @return			ignore duplicate?
	 */
	
	protected boolean ignoreDuplicateEndByte()
	{
		return( true );
	}
	
	
	/**
	 * Inflate (duplicate) End bytes in buffer 
	 *
	 * @param	writeBuffer		buffer to put the inflated data into
	 * @param	buffer			original data
	 * @param	offset			starting point in original data buffer
	 * @param	length			length of original data buffer
	 * @param	maxLen			max length of write buffer
	 * @return					# bytes copied to writeBuffer, -1 if error
	 * @throws GPSSerialPortException
	 */
	
	protected int inflateDuplicateEndBytes( byte[] writeBuffer, byte[] buffer, int offset, int length, int maxLen ) throws GPSSerialPortException
	{
		int		newLen	= 0;
		int		i;
		
		for( i = offset; i < length; i++ ) {
			if( i != offset && i < length - 2 && buffer[ i ] == END_PACKET1 ) {
				if( newLen < maxLen ) {
					writeBuffer[ newLen ] = END_PACKET1;
					newLen++;
				} else {
					newLen = -1;
					throw( new GPSSerialPortException( GPSSerialPortException.EXCEPTION_BUFFER_OVERRUN ) );
				}
			} 
			if( newLen < maxLen ) {
				writeBuffer[ newLen ] = buffer[ i ];
				newLen++;
			} else {
				newLen = -1;
				throw( new GPSSerialPortException( GPSSerialPortException.EXCEPTION_BUFFER_OVERRUN ) );
			}
		}
		
		return( newLen );
	}
	
	
	/**
	 * Append a trailing byte after the end byte?  
	 *
	 * @return			append trailing end byte?
	 */
	
	protected boolean appendTrailingEndByte()
	{
		return( true );
	}
	
	
	/**
	 * Get the trailing byte after the end byte?  
	 *
	 * @return			trailing byte?
	 */
	
	protected byte getTrailingEndByte()
	{
		return( END_PACKET2 );
	}
	
	
	/**
	 * onPacketNotify will be called when a complete packet has been read from the serial port
	 *
	 * @param		packet		byte array for packet
	 * @param		len			number of bytes in packet
	 */
	
	protected void onPacketNotify( byte[] packet, int len )
	{
		boolean				handled = false;
		GarminPacket		garminPacket;
		int					errCode;
		boolean				checksumOK;
		boolean				lengthOK;
		
		garminPacket = GarminPacket.createInstance( this, packet, len );
		
		if( garminPacket != null ) {	// Nothing to do?
			
			checksumOK 	 = garminPacket.validateChecksum();
			lengthOK	 = garminPacket.validateLength();
			
			if( !checksumOK || !lengthOK ) { 					// invalid packet
				if( Compile.DEBUG ) {
					debugDisplay( "Invalid Garmin Packet, " + ( checksumOK ? "Bad Length" : "Bad Checksum" ) );
					if( !checksumOK ) {
						debugDisplay( " Checksum Found: " + charToHex( (char)garminPacket.getChecksum() ) + ", Calc: " + charToHex( (char)garminPacket.calcChecksum() ) );
					}
					debugDisplay( "   Packet: " + garminPacket.toString() ) ;
				}
				nack.setRespondToPacket( garminPacket );
				try { 
					sendPacket( nack );
				}
				catch( GPSException exception ) {
					postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_ERROR, nack.toString(), exception ) );
				}
				if( !checksumOK ) {
					errCode = GPSPacketException.EXCEPTION_INVALID_CHECKSUM;
				} else {
					errCode = GPSPacketException.EXCEPTION_PACKETLENGTH_INVALID;
				}
				postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_ERROR, garminPacket.toString(), new GPSPacketException( errCode ) ) );	
			} else {
				if( Compile.DEBUG ) {
					debugDisplay( "Received Packet ID: " + garminPacket.getPacketIDString( protocol ) );
				}
				if( !(garminPacket instanceof AckGarminPacket || garminPacket instanceof NackGarminPacket ) ) {
					ack.setRespondToPacket( garminPacket );
					try {
						sendPacket( ack );
					}
					catch( GPSException exception ) {
						postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_ERROR, ack.toString(), exception ) );
					}
					if( Compile.DEBUG ) {
						debugDisplay( "Sent Ack to ID: " + garminPacket.getPacketIDString( protocol ) );
					}
				}
				
				if( isEventListened( GPSEvent.GPS_EVENT_GARMIN_PACKET ) ) {
					postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_GARMIN_PACKET, garminPacket ) );
				}
				
				garminPacket.process( this );
			}
		}
	}
	
	
	/**
	 * initialize Garmin GPS
	 *
	 */
	
	protected void initialize()
	{
		try {
			sendPacket( productRequest );
		}
		catch( GPSException exception ) {
			postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_ERROR, productRequest.toString(), exception ) );
		}
	}
	
	
	/**
	 * Send Packet
	 *
	 *
	 * @param	packet		packet to be sent
	 */
	
	void sendPacket( GarminPacket packet ) throws GPSSerialPortException, GPSConnectionException
	{
		writeSerial( packet.getPacketBuffer(), 0, packet.getPacketBufferLength() );
		
		if( Compile.DEBUG ) {
			debugDisplay( "Sent Packet ID: " + packet.getPacketIDString( protocol ) );
		}
	}
	
	
	//***************************************************************************
	//*
	//* 	Packet processing methods (callbacks)
	//*
	//***************************************************************************/
	
	
	/**
	 * Process a Product Data packet
	 *
	 * @param		packet	Product Data packet
	 */
	
	public void processInitPacket( Object packet )
	{
		productData = (ProductDataGarminPacket)packet;
		
		protocol.setProtocolTable( productData.getProductID(), productData.getSoftwareVersion() );
	}
	
	
	/**
	 * Process a Protocol Array packet
	 *
	 * @param		pkt		Protocol Array packet
	 */
	
	public void processCapabilitiesPacket( Object pkt )
	{
		
		protocol.processCapabilitiesPacket( (ProtocolArrayGarminPacket)pkt );
		
		if( Compile.INCLUDE_GARMIN_PACKET_PVT ) {
			startPVTMonitoring();
		}
	}
	
	
	/**
	 * Start PVT monitoring
	 *
	 */
	
	private void startPVTMonitoring()
	{
		if( Compile.INCLUDE_GARMIN_PACKET_PVT ) {
			CommandGarminPacket			command;
			
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_PVT ) ) {
				command = new CommandGarminPacket( protocol );
				
				command.setCommandType( Protocol.COMMAND_PVT_START, protocol );
				
				try {
					sendPacket( command );
				}
				catch( GPSException exception ) {
					postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_ERROR, command.toString(), exception ) );
				}
			}
		}
	}
	
	
	//***************************************************************************
	//*
	//* 	Accessor Abstract Methods -  (implemented/overriden)
	//*
	//***************************************************************************/
	
	/**
	 * Get Manufacturer ID
	 *
	 * @return   Manufacturer Id
	 */
	
	public String getManufacturerID()
	{
		return( "Garmin" );
	}
	
	
	/**
	 * Get Product ID
	 *
	 * @return   Product Id
	 * @throws GPSOperationUnavailableException
	 */
	
	public int getProductID() throws GPSOperationUnavailableException
	{
		int		id = -1;
		
		if( productData != null ) {
			id = productData.getProductID();
		} else {
			throw( new GPSOperationUnavailableException() );
		}
		
		return( id );
	}
	
	
	/**
	 * Get Description
	 *
	 * @return   Description
	 * @throws GPSOperationUnavailableException
	 */
	
	public String getDescription() throws GPSOperationUnavailableException
	{
		String		desc = "null";
		
		if( productData != null && productData.getDescriptionCount() > 0 ) {
			desc = productData.getDescription( 0 );
		} else {
			throw( new GPSOperationUnavailableException() );
		}
		
		return( desc );
	}
	
	
	/**
	 * Get Software Version
	 *
	 * @return   Software Version
	 * @throws GPSOperationUnavailableException
	 */
	
	public int getSoftwareVersion() throws GPSOperationUnavailableException
	{
		int		sv = -1;
		
		if( productData != null ) {
			sv = productData.getSoftwareVersion();
		} else {
			throw( new GPSOperationUnavailableException() );
		}
		
		return( sv );
	}
	
	
	//***************************************************************************
	//*
	//* 	Waypoint Transfer Methods
	//*
	//***************************************************************************/
	
	/**
	 * Download Waypoints from the GPS unit.  
	 *
	 * @throws GPSException
	 */
	
	protected void downloadWaypointsImpl() throws GPSException
	{
		if( Compile.INCLUDE_GARMIN_DOWNLOAD_WAYPOINTS ) {
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_WAYPOINT_XFER ) ) {
				if( waypointTransferHandler == null ) {
					waypointTransferHandler = new WaypointTransferHandler( this );
				}
				
				if( !waypointTransferHandler.isTransferInProgress() ) {
					waypointTransferHandler.downloadWaypoints();
				} else {
					throw( new GPSOperationUnavailableException() );
				}
			} else {
				throw( new GPSFunctionUnavailableException() );
			}
		} else {
			throw( new GPSFunctionUnavailableException() );
		}
	}
	
	
	/**
	 * Start Downloading Waypoints from the GPS unit. 
	 *
	 * @throws GPSException
	 */
	
	protected boolean startDownloadWaypointsImpl() throws GPSException
	{
		boolean					ret		= true;
		
		if( Compile.INCLUDE_GARMIN_DOWNLOAD_WAYPOINTS ) {
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_WAYPOINT_XFER ) ) {
				if( waypointTransferHandler == null ) {
					waypointTransferHandler = new WaypointTransferHandler( this );
				}
				
				if( !waypointTransferHandler.isTransferInProgress() ) {
					ret = waypointTransferHandler.startDownloadWaypoints();
				} else {
					throw( new GPSOperationUnavailableException() );
				}
			} else {
				throw( new GPSFunctionUnavailableException() );
			}
		} else {
			throw( new GPSFunctionUnavailableException() );
		}
		
		return( ret );
	}
	
	
	
	//***************************************************************************
	//*
	//* 	Route Transfer Methods
	//*
	//***************************************************************************/
	
	/**
	 * Download Routes from the GPS unit.  
	 *
	 * @throws GPSException
	 */
	
	protected void downloadRoutesImpl() throws GPSException
	{
		if( Compile.INCLUDE_GARMIN_DOWNLOAD_ROUTES ) {
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_ROUTE_XFER ) ) {
				if( routeTransferHandler == null ) {
					routeTransferHandler = new RouteTransferHandler( this );
				}
				
				if( !routeTransferHandler.isTransferInProgress() ) {
					routeTransferHandler.downloadRoutes();
				} else {
					throw( new GPSOperationUnavailableException() );
				}
			} else {
				throw( new GPSFunctionUnavailableException() );
			}
		} else {
			throw( new GPSFunctionUnavailableException() );
		}
	}
	
	
	/**
	 * Start Downloading Routes from the GPS unit. 
	 *
	 * @throws GPSException
	 */
	
	protected boolean startDownloadRoutesImpl() throws GPSException
	{
		boolean					ret		= true;
		
		if( Compile.INCLUDE_GARMIN_DOWNLOAD_ROUTES ) {
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_ROUTE_XFER ) ) {
				if( routeTransferHandler == null ) {
					routeTransferHandler = new RouteTransferHandler( this );
				}
				
				if( !routeTransferHandler.isTransferInProgress() ) {
					ret = routeTransferHandler.startDownloadRoutes();
				} else {
					throw( new GPSOperationUnavailableException() );
				}
			} else {
				throw( new GPSFunctionUnavailableException() );
			}
		} else {
			throw( new GPSFunctionUnavailableException() );
		}
		
		return( ret );
	}
	
	
	//***************************************************************************
	//*
	//* 	Track Log Transfer Methods
	//*
	//***************************************************************************/
	
	/**
	 * Download Tracks from the GPS unit.  
	 *
	 * @throws GPSException
	 */
	
	protected void downloadTracksImpl() throws GPSException
	{
		if( Compile.INCLUDE_GARMIN_DOWNLOAD_TRACKLOGS ) {
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_TRACKLOG_XFER ) ) {
				if( trackTransferHandler == null ) {
					trackTransferHandler = new TrackTransferHandler( this );
				}
				
				if( !trackTransferHandler.isTransferInProgress() ) {
					trackTransferHandler.downloadTracks();
				} else {
					throw( new GPSOperationUnavailableException() );
				}
			} else {
				throw( new GPSFunctionUnavailableException() );
			}
		} else {
			throw( new GPSFunctionUnavailableException() );
		}
	}
	
	
	/**
	 * Start Downloading Tracks from the GPS unit. 
	 *
	 * @throws GPSException
	 */
	
	protected boolean startDownloadTracksImpl() throws GPSException
	{
		boolean					ret		= true;
		
		if( Compile.INCLUDE_GARMIN_DOWNLOAD_TRACKLOGS ) {
			if( protocol.isAppProtocolSupported( Protocol.APP_PROTOCOL_TRACKLOG_XFER ) ) {
				if( trackTransferHandler == null ) {
					trackTransferHandler = new TrackTransferHandler( this );
				}
				
				if( !trackTransferHandler.isTransferInProgress() ) {
					ret = trackTransferHandler.startDownloadTracks();
				} else {
					throw( new GPSOperationUnavailableException() );
				}
			} else {
				throw( new GPSFunctionUnavailableException() );
			}
		} else {
			throw( new GPSFunctionUnavailableException() );
		}
		
		return( ret );
	}
	
	
}
